В РАЗРАБОТКЕНЕ ДЛЯ НОВИЧКОВНЕ ОБЯЗАТЕЛЬНО
Разработчику
Архитектору
📚 Лексика, типы, операторы, ключевые слова
1. Типы данных
Встроенные (примитивные) типы и их псевдонимы:
| C# псевдоним | Системный тип | Размер (бит) | Диапазон / Примечание |
|---|
bool | System.Boolean | 8 | true / false |
sbyte | System.SByte | 8 | −128 … 127 |
byte | System.Byte | 8 | 0 … 255 |
short | System.Int16 | 16 | −32 768 … 32 767 |
ushort | System.UInt16 | 16 | 0 … 65 535 |
int | System.Int32 | 32 | −2 147 483 648 … 2 147 483 647 |
uint | System.UInt32 | 32 | 0 … 4 294 967 295 |
long | System.Int64 | 64 | −2⁶³ … 2⁶³−1 |
ulong | System.UInt64 | 64 | 0 … 2⁶⁴−1 |
nint | System.IntPtr | 32/64 | зависит от архитектуры (указательный размер) |
nuint | System.UIntPtr | 32/64 | зависит от архитектуры |
char | System.Char | 16 | UTF-16 символ (один элемент кодировки) |
float | System.Single | 32 | ~±1.5 × 10⁻⁴⁵ … ±3.4 × 10³⁸, 7 цифр точности |
double | System.Double | 64 | ~±5.0 × 10⁻³²⁴ … ±1.7 × 10³⁰⁸, 15–16 цифр |
decimal | System.Decimal | 128 | ±1.0 × 10⁻²⁸ … ±7.9 × 10²⁸, 28–29 цифр, для финансовых вычислений |
string | System.String | ref | ссылка на неизменяемую UTF-16 строку |
object | System.Object | ref | корневой тип всех ссылочных и упакованных значений |
Категории типов:
- Значимые (value types): структуры (
struct), перечисления (enum), простые типы (int, DateTime, Guid, Span<T>, UnsafePointer, nint).
- Ссылочные (reference types): классы (
class), интерфейсы (interface), делегаты (delegate), массивы, строки — все наследуются от System.Object.
- Указатели (
T*): доступны только в unsafe-контексте.
Специальные типы / конструкции:
| Конструкция | Описание |
|---|
dynamic | Отложенное связывание: проверка членов происходит во время выполнения. Наследуется от System.Object, но компилятор отключает статическую проверку. |
var | Не тип, а указание компилятору вывести тип на основе инициализатора. Работает только с локальными переменными. |
record | Синтаксический сахар для неизменяемых (по умолчанию) типов с генерацией Equals, GetHashCode, ToString, операторов ==/!=, with и (в record struct) copy-конструкторов. |
record struct | Значимый record, появился в C# 10. |
ref struct | Значимый тип, который не может покидать стек: не может быть полем класса, элементом массива, кэширован в async, упакован в object. Примеры: Span<T>, ReadOnlySpan<T>. |
required (C# 11) | Модификатор свойства или поля в record/class/struct, требующий обязательной инициализации либо в инициализаторе объекта, либо в конструкторе. |
init | Модификатор метода-сеттера: допускает присваивание только во время инициализации объекта (в конструкторе или инициализаторе). |
readonly (для struct) | Гарантирует, что экземпляр структуры не будет изменён после инициализации — все поля readonly, методы readonly. |
readonly ref | Возвращаемое значение ссылки на readonly-данные — запрещает их модификацию. |
scoped (C# 12) | Указывает, что ссылка (в т.ч. ref, Span<T>) не должна выходить за пределы текущего блока/метода. |
2. Ключевые слова языка
Обязательные (не могут быть идентификаторами):
abstract, as, base, bool, break, byte, case, catch, char, checked, class, const, continue, decimal, default, delegate, do, double, else, enum, event, explicit, extern, false, finally, fixed, float, for, foreach, goto, if, implicit, in, int, interface, internal, is, lock, long, namespace, new, null, object, operator, out, override, params, private, protected, public, readonly, ref, return, sbyte, sealed, short, sizeof, stackalloc, static, string, struct, switch, this, throw, true, try, typeof, uint, ulong, unchecked, unsafe, ushort, using, virtual, void, volatile, while
Контекстные ключевые слова (могут быть идентификаторами в других контекстах):
add, alias, and, ascending, async, await, by, descending, dynamic, equals, from, get, global, group, init, into, join, let, nameof, nint, not, nuint, on, or, orderby, partial, record, remove, required, select, set, unmanaged, value, var, when, where, with, yield
Примечание: global используется в global::System.Int32 для разрешения коллизий имён с помощью глобального пространства имён.
3. Модификаторы доступа
| Модификатор | Применяется к | Видимость |
|---|
public | Любой член | Везде |
protected | Член класса | Внутри содержащего класса и производных классов |
internal | Любой член | В пределах сборки (assembly) |
protected internal | Член класса | Внутри сборки или в производных классах (в любой сборке) |
private | Любой член | Только внутри содержащего типа |
private protected | Член класса | Только в производных классах внутри той же сборки |
Уровень по умолчанию:
- Для
class/struct: internal
- Для членов:
private
4. Модификаторы членов (не доступа)
| Модификатор | Применение | Эффект |
|---|
static | класс, метод, поле, свойство, событие, конструктор, оператор | Принадлежит типу, а не экземпляру |
readonly | поле, struct | Поле инициализируется только в объявлении или конструкторе; readonly struct — все поля readonly, все методы readonly |
const | поле | Компилируемая константа (только для bool, целых, char, string, enum). Подставляется в IL как литерал. |
virtual | метод, свойство, событие, индексатор | Разрешает переопределение в производном классе |
override | метод, свойство, событие, индексатор | Явное переопределение virtual/abstract члена |
abstract | класс, метод, свойство, событие, индексатор | Тип или член без реализации — должен быть переопределён в неабстрактном классе |
sealed | класс, метод (override), record | Класс — запрет на наследование; метод — запрет дальнейшего переопределения |
partial | class, struct, interface, метод | Разделение определения на несколько файлов или частей (требуется одинаковое имя и сигнатура) |
extern | метод | Реализация вынесена за пределы кода (P/Invoke, DllImport) |
unsafe | блок, метод, тип | Включение режима указателей и небезопасных операций |
volatile | поле | Запрещает компилятору и CPU применять оптимизации, нарушающие порядок чтения/записи (для многопоточной синхронизации без lock) |
async | метод, локальная функция | Указывает, что метод может содержать await и возвращает Task/Task<T>/ValueTask/ValueTask<T>/IAsyncEnumerable<T>/void (только для event handlers) |
ref | параметр, возвращаемое значение, локальная переменная, struct | Передача/возврат по ссылке (не копируется значение) |
in | параметр | ref readonly — передача по ссылке без возможности модификации |
out | параметр | Выходной параметр: инициализация обязательна в методе, не читается до присвоения |
params | последний параметр массива | Позволяет вызывать метод с переменным числом аргументов (например, params int[] values) |
required | свойство, поле (в C# 11+) | Обязательная инициализация при создании объекта |
scoped | параметр (ref, Span<T> и др.) | Гарантия, что ссылка не покинет текущую область |
5. Операторы
Приоритет (уменьшается сверху вниз):
| Приоритет | Операторы | Примечания |
|---|
| 20 | x.y, x?.y, f(x), a[x], x++, x--, new, typeof, checked, unchecked, default, nameof, sizeof, stackalloc, x->y, x! (null-forgiving), x is T, x as T | Постфиксные, вызовы, доступ по индексу, создание |
| 19 | +x, -x, !x, ~x, ++x, --x, ^x (index from end), await, &x, *x, (T)x | Унарные, приведения, указательные операции |
| 18 | x * y, x / y, x % y | Мультипликативные |
| 17 | x + y, x - y | Аддитивные |
| 16 | x << y, x >> y | Побитовые сдвиги |
| 15 | x < y, x <= y, x > y, x >= y, is, as | Сравнение, проверка типа |
| 14 | x == y, x != y | Равенство |
| 13 | x & y | Побитовое И (для bool — несокращённое логическое И) |
| 12 | x ^ y | Побитовое исключающее ИЛИ (XOR) |
| 11 | x | y | Побитовое ИЛИ (для bool — несокращённое логическое ИЛИ) |
| 10 | x && y | Сокращённое логическое И |
| 9 | x || y | Сокращённое логическое ИЛИ |
| 8 | x ?? y | Null-coalescing |
| 7 | c ? a : b | Тернарный условный |
| 6 | x = y, x op= y, x ??= y, x ?= y (нет), => — не оператор | Присваивание (все возвращают присвоенное значение) |
| 5 | yield return, yield break | Только в итераторах (IEnumerable<T>, IAsyncEnumerable<T>) |
| 4 | => | Лямбда-выражение (не входит в приоритет таблицы как оператор) |
Специальные операторы и конструкции:
| Оператор / Конструкция | Описание |
|---|
is | Проверка типа, сопоставление с образцом: obj is string s, x is > 0 and < 10, x is int[] { Length: 5 } |
as | Безопасное приведение: возвращает null, если несовместимо (только для ссылочных типов и Nullable<T>) |
?? | Null-coalescing: a ?? b → если a null, то b, иначе a |
??= | Null-coalescing assignment: a ??= b ⇔ a = a ?? b |
?. / ?[] | Условный оператор доступа: obj?.Prop?[0]?.Method() — прерывает цепочку при null |
! | Null-forgiving (suppression) operator — отключает предупреждения NRT (nullable reference types) |
^ | Index from end: arr[^1] — последний элемент; ^n = Length - n (требуется using System;) |
.. | Range operator: arr[1..^1] — срез от индекса 1 до предпоследнего; ..3, 2.., .. |
() | Вызов, группировка, приведение, кортеж |
[] | Доступ по индексу, объявление массива, атрибуты |
{} | Инициализатор объекта, коллекции, анонимного типа, блок кода |
=> | Expression-bodied member, лямбда |
nameof(...) | Получение имени идентификатора как string (без hardcode) — работает на этапе компиляции |
default(T) / default | Значение по умолчанию для типа: 0, false, null, обнуленная структура |
sizeof(T) | Размер типа в байтах — только для неуправляемых типов и в unsafe или Unsafe.SizeOf<T>() в безопасном коде |
stackalloc T[n] | Выделение массива на стеке (только в unsafe или для Span<T> в безопасном коде с ReadOnlySpan<byte> инициализаторами) |
&x, *p | Адрес и разыменование — только в unsafe |
📚 Члены типов
1. Поля (Fields)
Синтаксис:
[<атрибуты>] [<модификаторы>] <тип> <имя> [= <инициализатор>];
Типы полей:
| Тип | Пример | Примечание |
|---|
| Обычное | private int _count; | Хранит состояние экземпляра |
| Статическое | public static readonly DateTime Epoch = new(1970, 1, 1); | Принадлежит типу |
readonly | public readonly Guid Id = Guid.NewGuid(); | Инициализируется только в объявлении или конструкторе |
const | public const double Pi = 3.141592653589793; | Компилируемая константа (только для примитивов и string) |
volatile | private volatile bool _isCancelled; | Запрещает оптимизации, нарушающие порядок чтения/записи |
required (C# 11) | public required string Name; | Обязательное к инициализации при создании объекта (new X { Name = "..." } или конструктор) |
init (через field в C# 12) | public string Name { get; init; } = field ?? ""; — field ссылается на неявное поле | Только в свойствах с init или get/set |
ref struct поле | Запрещено — ref struct не может быть полем обычного класса/структуры | |
ref поле | Нельзя объявить напрямую, но можно через unsafe-указатель или ref-параметр/локальную переменную | |
fixed (буфер) | public unsafe fixed byte Buffer[256]; | Только в unsafe, только в struct, выделяется инлайн |
Инициализация:
- Порядок инициализации:
- Статические поля → статический конструктор
- Поля экземпляра → конструктор базового класса (
base(...)) → конструктор текущего класса
required поля не инициализируются автоматически — контроль на этапе компиляции (если не указано при создании, ошибка CS9035).
2. Свойства (Properties)
Синтаксис:
[<атрибуты>] [<модификаторы>] <тип> <Имя>
{
[<модификаторы>] get { <тело> }
[<модификаторы>] set { <тело> }
[<модификаторы>] init { <тело> }
}
public string Name => _name;
public int Length => _items?.Length ?? 0;
Виды свойств:
| Тип | Пример | Примечание |
|---|
| Автосвойство | public string Name { get; set; } | Компилятор генерирует скрытое поле <Name>k__BackingField |
init-свойство | public string Id { get; init; } | Присваивание только при инициализации (new X { Id = "..." }, первичный конструктор, with) |
required-свойство | public required string Login { get; set; } | Обязательное заполнение при создании — иначе ошибка компиляции |
readonly автосвойство | public string Version { get; } = "1.0"; | Только get; инициализация — в конструкторе или при объявлении |
virtual/abstract/override | public virtual int Count => _items.Count; | Поддерживает переопределение |
| Выражение | public bool IsEmpty => Count == 0; | Краткая форма для get |
| Полное (с логикой) | private string _name; public string Name { get => _name; set => _name = value?.Trim() ?? ""; } | Контроль доступа и валидации |
| Индексатор | public T this[int index] => _items[index]; | Свойство с параметром — имя всегда this |
Ключевые слова в set/init:
value — неявный параметр, тип совпадает с типом свойства.
field (C# 12) — ссылка на неявное поле автосвойства (только в get/set/init, не в обычных свойствах с явным полем):
public string Name
{
get => field?.ToUpper();
init => field = value?.Trim() ?? "";
}
3. События (Events)
Синтаксис:
[<атрибуты>] [<модификаторы>] event <делегат> <Имя>;
private EventHandler<MyEventArgs> _myEvent;
public event EventHandler<MyEventArgs> MyEvent
{
add => _myEvent += value;
remove => _myEvent -= value;
}
Стандартные делегаты:
| Делегат | Сигнатура | Применение |
|---|
EventHandler | void Method(object sender, EventArgs e) | Без данных события |
EventHandler<T> | void Method(object sender, T e) where T : EventArgs | С данными (T наследует EventArgs) |
| Пользовательский | delegate void MyDelegate(string msg); | Редко — предпочтительно Action<T>/Func<T,R> или EventHandler<T> |
Правила:
- Внутри класса событие вызывается как делегат:
MyEvent?.Invoke(this, args);
- За пределами — только
+= и -= (компилятор запрещает прямой вызов и присваивание).
event-поле нельзя явно инициализировать (= null избыточно — по умолчанию null).
- В многопоточной среде рекомендуется копировать делегат перед вызовом:
var handler = MyEvent;
handler?.Invoke(this, args);
Или использовать Interlocked.CompareExchange / Volatile.Read.
4. Методы (Methods)
Синтаксис:
[<атрибуты>] [<модификаторы>] <возвращаемый тип> <Имя>([<параметры>])
{
<тело>
}
public int Square(int x) => x * x;
Параметры:
| Способ передачи | Синтаксис | Поведение |
|---|
| По значению (по умолчанию) | void M(int x) | Копия значения (для ссылочных — копия ссылки) |
ref | void M(ref int x) | Передача по ссылке — изменения влияют на аргумент |
out | bool TryParse(string s, out int result) | Выходной параметр — должен быть проинициализирован в методе |
in | void M(in ReadOnlySpan<byte> data) | ref readonly — нельзя модифицировать, но избегается копирование |
params | void Log(params string[] messages) | Последний параметр массива; вызов: Log("a", "b", "c") |
| По умолчанию | void M(int x = 10) | Значение подставляется, если аргумент не указан |
| Именованный | M(x: 5, y: "test") | Порядок не важен; сочетается с params (Log(severity: Info, messages: "a", "b")) |
Особенности методов:
| Вид | Пример | Примечание |
|---|
| Статический | public static int Max(int a, int b) => Math.Max(a, b); | Нет this |
| Виртуальный | public virtual void Draw() { ... } | Может быть переопределён |
| Абстрактный | public abstract void Execute(); | Только в abstract-классе, без тела |
| Переопределённый | public override string ToString() => $"Point({X}, {Y})"; | Должен соответствовать сигнатуре базового virtual/abstract |
| Запечатанный | public sealed override void Draw() { ... } | Запрещает дальнейшее переопределение |
| Частичный | partial void Validate(); | Может быть определён в другой части partial class; если не реализован — компилятор удаляет вызовы |
| Внешний | public static extern int GetTickCount(); [DllImport("kernel32")] ... | Реализация в нативном коде |
| Асинхронный | public async Task<int> DownloadAsync() | Должен содержать await или возвращать Task/ValueTask/IAsyncEnumerable |
| Итераторный | public IEnumerable<int> Range(int start, int count) → yield return i; | Компилятор генерирует IEnumerator<T> |
| Локальная функция | void Outer() { int Helper() => 42; } | Доступна только в методе; может быть static, async, iterator |
| Обобщённый | public T FirstOrDefault<T>(IEnumerable<T> source, Func<T, bool> pred) | Параметры типа: where T : class, struct, new(), IDisposable, notnull, unmanaged |
Generic constraints (ограничения параметров типа):
| Ограничение | Пример | Значение |
|---|
where T : class | class Box<T> where T : class | Ссылочный тип (включая string, делегаты, массивы) |
where T : class? | (C# 9+, NRT) | Ссылочный тип, допускает null |
where T : struct | Span<T> where T : struct | Неуправляемый значимый тип (не Nullable<T>) |
where T : unmanaged | (C# 7.3) | Значимый тип без ссылок и GC-трекинга — можно использовать в unsafe |
where T : new() | T Create<T>() where T : new() => new T(); | Должен иметь публичный конструктор без параметров |
where T : IDisposable | void Use<T>(T obj) where T : IDisposable | Должен реализовывать интерфейс |
where T : notnull | (C# 8+) | Запрещает null даже в nullable-контексте |
where T : U | void Copy<T, U>(T src, U dst) where T : U | T должен быть U или производным от U |
5. Конструкторы (Constructors)
Виды:
| Тип | Синтаксис | Примечание |
|---|
| Экземпляра | public Point(int x, int y) { X = x; Y = y; } | Вызывается при new |
| Статический | static MyClass() { ... } | Вызывается один раз перед первым использованием типа |
| Первичный (C# 12) | public record Person(string Name, int Age) { ... } | Сокращённая форма для record и class/struct — параметры становятся public readonly полями/свойствами |
| Делегирование | public Point() : this(0, 0) { } | Цепочка через this(...) или base(...) |
Поведение:
- Если нет конструктора экземпляра — компилятор генерирует публичный без параметров (
public C() : base() {}), кроме record, где генерируется первичный (если указаны параметры) или без параметров.
record с первичным конструктором автоматически создаёт:
- Поля/свойства для параметров (по умолчанию
public init для record class, public readonly для record struct)
Deconstruct
ToString, Equals, GetHashCode, ==, !=, оператор with
struct всегда имеет неявный конструктор без параметров (нельзя переопределить), но можно определить свой — при этом поля должны быть инициализированы до выхода.
6. Финализаторы (Finalizers)
- Компилируется в
protected override void Finalize().
- Вызывается GC асинхронно, не гарантируется порядок и момент.
- Не использовать для управляемых ресурсов — только для
IntPtr, хэндлов ОС.
- Рекомендуется использовать
IDisposable + SafeHandle вместо финализатора.
- Если реализуете
IDisposable, финализатор должен вызывать Dispose(false).
7. Операторы (Operators)
Перегружаемые:
| Оператор | Сигнатура | Примечание |
|---|
| Унарные | public static T operator +(T a) | +, -, !, ~, ++, --, true, false |
| Бинарные | public static T operator +(T a, T b) | +, -, *, /, %, &, |, ^, <<, >>, ==, !=, <, >, <=, >= |
| Приведения | public static explicit operator T(U u) public static implicit operator T(U u) | explicit — требует явного приведения; implicit — не требует |
Правила:
== и != должны быть перегружены парно.
- Если перегружены
==/!=, то рекомендуется переопределить Equals и GetHashCode.
true/false — нужны для &&/\|\| (компилятор преобразует a && b → a ? b : false при наличии true/false).
implicit приведение должно быть безопасным (без потерь, исключений).
- Нельзя перегрузить:
=, &&, \|\|, ??, ?., ->, ?:, new, typeof, sizeof, is, as, checked, unchecked.
8. Вложенные типы
public class Outer
{
private int _x;
public class Nested { }
public struct NestedStruct { }
public interface INested { }
public delegate void NestedDelegate();
public enum NestedEnum { A, B }
}
- Доступность по умолчанию:
private.
- Вложенные типы наследуют generic-параметры внешнего типа:
class Outer<T>
{
class Inner : IEnumerable<T> { ... }
}
private protected вложенный тип виден только в производных классах внутри той же сборки.
9. Частичные (partial) методы
partial void OnValidate();
partial void OnValidate()
{
}
- Должны возвращать
void.
- Могут иметь
out-параметры, но не ref/in/params.
- Могут быть
static, но не virtual, abstract, override.
- Используются в source generators (например, для hook-ов инициализации).
10. Записи (record, record struct)
| Фича | record class | record struct |
|---|
| Неизменяемость по умолчанию | init-свойства | readonly поля (если нет set) |
with-выражение | Да (var y = x with { Name = "new" };) | Да (копирует все поля) |
| Первичный конструктор | record R(string Name); → public string Name { get; init; } | То же, но поля readonly |
Deconstruct | Автогенерируется: void Deconstruct(out string Name) | Автогенерируется |
Equals/GetHashCode | Сравнение по значению полей/свойств | То же |
==/!= | Перегружены | Перегружены |
| Наследование | Да (но только от record или object) | Нет (структуры не наследуются) |
PrintMembers | protected virtual bool PrintMembers(StringBuilder sb) | Нет (но можно определить) |
📚 Управление памятью и производительность
1. Упаковка (boxing) и распаковка (unboxing)
Что происходит:
- Boxing: преобразование значимого типа в
object или интерфейс → выделение памяти в куче, копирование значения, возврат ссылки.
int x = 42;
object boxed = x;
- Unboxing: извлечение значения из упакованного объекта → проверка типа (
isinst), копирование значения.
Стоимость:
| Операция | Стоимость | Примечание |
|---|
| Boxing | ~100–300 тактов | Выделение памяти, вызов GC.Alloc, копирование |
| Unboxing | ~10–30 тактов | Проверка типа (RTTI), копирование |
| Повторное boxing одного и того же значения | Не кэшируется (кроме случаев ниже) | object a = 5; object b = 5; ReferenceEquals(a, b) → false |
Кэширование значений (CLR-оптимизация):
Только для:
bool: true, false
char: U+0000 … U+007F (0–127)
- Целые (
sbyte, byte, short, ushort, int): значения от –128 до 127
object a = 100, b = 100;
Console.WriteLine(ReferenceEquals(a, b));
object c = 300, d = 300;
Console.WriteLine(ReferenceEquals(c, d));
Как избежать:
- Использовать обобщённые коллекции (
List<int>, а не ArrayList).
- Избегать
object/IComparable/IEquatable<object> для значимых типов.
- Применять
Span<T>, ref, in, ref struct.
- В интерфейсах использовать generic-ограничения:
where T : IEquatable<T>, а не IEquatable<object>.
2. IDisposable, IAsyncDisposable и управление ресурсами
Стандартный шаблон (Dispose pattern):
public class FileStreamWrapper : Stream, IDisposable
{
private FileStream? _stream;
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_stream?.Dispose();
}
_stream = null;
_disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
~FileStreamWrapper()
{
Dispose(disposing: false);
}
public async ValueTask DisposeAsync()
{
if (_stream != null)
{
await _stream.DisposeAsync().ConfigureAwait(false);
_stream = null;
}
_disposed = true;
GC.SuppressFinalize(this);
}
}
Ключевые правила:
| Правило | Примечание |
|---|
Dispose() должен быть идемпотентным | Повторные вызовы — безопасны |
GC.SuppressFinalize(this) — обязательно в Dispose() | Иначе объект попадёт в финализационную очередь, замедляя GC |
IAsyncDisposable должен возвращать ValueTask, а не Task | Для избежания аллокаций |
await using → вызывает DisposeAsync() | Аналог using, но для асинхронного освобождения |
using var x = ...; — синтаксический сахар | Блок using до конца текущей области видимости |
Когда НЕ нужен финализатор:
- Если ресурсы обёрнуты в
SafeHandle (рекомендуется).
- Если используются только управляемые ресурсы (
Stream, HttpClient, SqlConnection и т.п.).
SafeHandle — предпочтительный способ:
public sealed class MySafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private MySafeHandle() : base(true) { }
protected override bool ReleaseHandle() => NativeMethods.CloseHandle(handle);
}
[DllImport("kernel32")]
public static extern MySafeHandle CreateFile(...);
→ Автоматически интегрируется с GC, thread-safe, предотвращает утечки при AppDomain unload.
3. Span<T>, ReadOnlySpan<T>, Memory<T>, ReadOnlyMemory<T>
Сравнительная таблица:
| Тип | Где может храниться | Управляемый? | Може ли быть полем? | Може ли выходить из метода? | Использование |
|---|
Span<T> | стек, неуправляемая память, массив | нет — ref struct | ❌ | ❌ (кроме ref return) | Быстрые операции в пределах метода |
ReadOnlySpan<T> | то же | нет — ref struct | ❌ | ❌ | Только чтение |
Memory<T> | управляется GC | ✅ | ✅ | ✅ | Передача между асинхронными операциями |
ReadOnlyMemory<T> | то же | ✅ | ✅ | ✅ | Только чтение |
Создание:
var arr = new byte[1024];
Span<byte> span = arr.AsSpan();
ReadOnlySpan<byte> roSpan = arr.AsSpan().Slice(10, 100);
ReadOnlySpan<char> strSpan = "Hello".AsSpan();
unsafe
{
void* ptr = NativeMemory.Alloc(1024);
Span<byte> span = new(ptr, 1024);
NativeMemory.Free(ptr);
}
Span<byte> buffer = stackalloc byte[256];
Memory<byte> mem = new byte[1024];
Span<byte> span = mem.Span;
Ограничения Span<T>:
- Нельзя:
- быть полем класса/обычной структуры
- быть элементом массива
- использоваться в
async/yield методах
- передаваться в
lambda, local function, try/catch/finally (в некоторых случаях)
- использоваться как обобщённый аргумент (
List<Span<T>> — ошибка)
→ Решение: использовать Memory<T> для передачи, .Span — для локальной обработки.
Пример безопасного использования:
public static bool TryParseHeader(ReadOnlySpan<byte> data, out int length)
{
if (data.Length < 4) { length = 0; return false; }
length = BinaryPrimitives.ReadInt32LittleEndian(data);
return length >= 0;
}
4. stackalloc, ArrayPool<T>, MemoryPool<T>
stackalloc:
Span<byte> buffer = stackalloc byte[1024];
int n = Math.Min(count, 1024);
Span<byte> temp = stackalloc byte[n];
- Максимальный размер по умолчанию: ~1 МБ (ограничение Windows x64), но зависит от платформы.
- Не вызывает
GC, но переполнение стека ⇒ StackOverflowException.
- В безопасном коде доступен только для
Span<T> с T = byte, sbyte, char, ushort, short, uint, int, ulong, long, nint, nuint, float, double.
ArrayPool<T>.Shared:
var array = ArrayPool<byte>.Shared.Rent(2048);
try
{
}
finally
{
ArrayPool<byte>.Shared.Return(array, clearArray: false);
}
- Пулы: глобальный (
Shared) или пользовательский (ArrayPool<T>.Create(maxArrayLength, maxArraysPerBucket)).
clearArray: true — обнулить массив перед возвратом (защита от утечки данных).
- Размеры: округляются до ближайшего бакета (256, 512, 1024, …, 1 048 576).
MemoryPool<T> (для Memory<T>):
var owner = MemoryPool<byte>.Shared.Rent(1024);
try
{
Memory<byte> mem = owner.Memory;
}
finally
{
owner.Dispose();
}
- Особенно полезно для
IAsyncEnumerable<T>, PipeWriter, ArrayPool-бэкенд.
5. System.Runtime.CompilerServices.Unsafe
Пакет: System.Runtime.CompilerServices.Unsafe (входит в .NET Standard 2.1+)
Основные методы:
| Метод | Подпись | Назначение |
|---|
As<TFrom, TTo> | TTo As<TFrom, TTo>(ref TFrom source) | Интерпретация битов одного типа как другого (без копирования) |
AsPointer | void* AsPointer<T>(ref T value) | Получение указателя на переменную (без unsafe) |
AsRef | ref T AsRef<T>(void* source) | Преобразование указателя в ref |
SizeOf<T> | int SizeOf<T>() | Размер типа в байтах (без unsafe, работает для всех типов) |
Add<T> / Subtract<T> | ref T Add<T>(ref T source, int offset) | Арифметика указателей через ref |
Read<T> / Write<T> | T Read<T>(void* source) | Чтение/запись по неуправляемому адресу |
CopyBlock / InitBlock | void CopyBlock(void* dest, void* src, uint size) | memcpy, memset на уровне IL |
Примеры:
unsafe
{
byte* src = ...;
byte* dst = ...;
Unsafe.CopyBlock(dst, src, (uint)1024);
}
ReadOnlySpan<byte> data = ...;
int value = Unsafe.ReadUnaligned<int>(ref MemoryMarshal.GetReference(data));
T CreateDefault<T>() => Unsafe.As<byte, T>(ref Unsafe.AsRef<byte>(null));
Важно: Unsafe не отключает проверки границ массивов. Для этого — Unsafe.Add(ref arr[0], index) вместо arr[index].
6. Подсказки сборщику мусора и JIT
Атрибуты:
| Атрибут | Применение | Эффект |
|---|
[ThreadStatic] | static поле | Каждый поток — своя копия |
[ThreadLocal] | static поле (ThreadLocal<T>) | То же, но с отложенной инициализацией |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | Метод | Подсказка JIT встраивать метод (не гарантируется) |
[MethodImpl(MethodImplOptions.AggressiveOptimization)] | Метод | JIT применяет агрессивные оптимизации (даже в debug) |
[MethodImpl(MethodImplOptions.NoInlining)] | Метод | Запрет встраивания (для профилирования, security) |
[MethodImpl(MethodImplOptions.NoOptimization)] | Метод | Отключить оптимизации (только для отладки) |
Методы GC:
| Метод | Применение |
|---|
GC.KeepAlive(object) | Продлить жизнь объекта до точки вызова (часто после P/Invoke) |
GC.SuppressFinalize(object) | Отменить финализацию (в Dispose()) |
GC.ReRegisterForFinalize(object) | Повторно поставить в очередь финализации |
GC.AllocateUninitializedArray<T>(int, bool) | Выделить массив без инициализации default(T) (для struct, где default = 0) |
GC.AllocateArray<T>(int, bool) | Выделить массив, избегая GC pressure (в .NET 6+) |
GC.GetGCMemoryInfo() | Статистика по поколениям, паузам, фрагментации |
JIT-подсказки через код:
- Использовать
ReadOnlySpan<T> вместо string/array для hot-путей.
- Избегать виртуальных вызовов в циклах (JIT не может devirtualize без
sealed или final).
- Использовать
ValueTask вместо Task для синхронного случая.
- Размещать часто используемые поля вместе (cache-line locality).
7. ref struct, ref fields, scoped (C# 11–12)
ref struct — правила:
- Может содержать только:
- другие
ref struct
ref T, ref readonly T
T* (в unsafe)
fixed-буферы
- Нельзя:
- реализовывать интерфейсы (даже
IDisposable — но можно ref-расширения Dispose)
- быть захваченным в замыкании
- использоваться в
async/yield
- быть обобщённым параметром
- быть возвращённым из метода (кроме
ref return)
ref fields (C# 11):
public ref struct RefWrapper
{
public ref int Value;
}
- Позволяет хранить ссылку на переменную внутри
ref struct.
- Требует осторожности: ссылка может стать "висячей".
scoped (C# 12):
Указывает, что ссылка не покидает текущую область:
public static scoped ref int Max(scoped ref int a, scoped ref int b)
=> ref (a > b ? ref a : ref b);
public static void Process(scoped Span<byte> data)
{
}
Поддерживаемые типы:
ref T, ref readonly T
Span<T>, ReadOnlySpan<T>
scoped ref struct (в параметрах)
field (C# 12) — доступ к неявному полю автосвойства:
public string Name
{
get => field?.ToUpperInvariant() ?? "";
init => field = value?.Trim() ?? "";
}
→ Работает только в get/set/init, не в обычных свойствах с явным полем.
📚 Асинхронность
1. Основные типы асинхронных операций
| Тип | Применение | Аллокации | Примечания |
|---|
Task | Асинхронная операция без результата | 1 (при первом await) | Кэшированная Task.CompletedTask — использовать вместо new Task() |
Task<T> | Асинхронная операция с результатом T | 1 (при первом await) | Если T — значимый тип, упаковка при await |
ValueTask | Оптимизация для синхронного случая | 0 (если синхронно завершено) | Обёртка: либо Task, либо ManualResetValueTaskSourceCore<T> |
ValueTask<T> | То же, с результатом | 0 (если синхронно) | Предпочтительно для hot-путей и интерфейсов (например, IAsyncEnumerable.MoveNextAsync()) |
IAsyncEnumerable<T> | Асинхронный поток значений | 1–N (в зависимости от реализации) | await foreach, yield return await, ConfigureAwait(false) в цикле |
ConfiguredCancelableAsyncEnumerable<T> | IAsyncEnumerable<T> с ConfigureAwait и CancellationToken | — | Через .ConfigureAwait(false).WithCancellation(ct) |
Когда использовать ValueTask вместо Task:
- Операция часто завершается синхронно (например, кэш hit).
- Метод вызывается в hot-path (более 10⁶ вызовов/сек).
- Интерфейс допускает
ValueTask (например, IAsyncDisposable.DisposeAsync() возвращает ValueTask).
Ограничения ValueTask:
- Нельзя
await дважды (кроме .AsTask()).
- Нельзя использовать
Task.Wait(), .Result, .GetAwaiter().GetResult() без проверки.
- Не поддерживает
Task.WhenAll, Task.WhenAny напрямую — конвертировать через .AsTask().
2. async/await: синтаксис и семантика
Синтаксис:
public async Task<int> DownloadAndParseAsync(string url, CancellationToken ct = default)
{
using var client = new HttpClient();
string json = await client.GetStringAsync(url, ct).ConfigureAwait(false);
return JsonSerializer.Deserialize<int>(json);
}
Ключевые правила:
| Правило | Примечание |
|---|
async метод должен содержать как минимум один await, или возвращать завершённую задачу напрямую | Иначе предупреждение IDE0078 (но компилируется) |
await распаковывает Task<T> → T, ValueTask<T> → T, Task/ValueTask → void | Исключение пробрасывается напрямую (не завёрнуто в AggregateException) |
await захватывает SynchronizationContext по умолчанию | В UI — возвращает в поток UI; в ASP.NET — в контекст запроса |
ConfigureAwait(false) — отменяет захват контекста | Рекомендуется в библиотеках, особенно в цепочках await |
async void — только для event handlers | В остальных случаях — async Task (иначе невозможно await, обработка исключений — через AppDomain.UnhandledException) |
State machine (IL):
- Компилятор генерирует
struct-машину состояний:
- Поля: параметры метода, локальные переменные,
awaiter, текущее состояние.
- Метод
MoveNext() — реализует логику после await.
- Размер state machine = сумма размеров захваченных переменных (оптимизация:
ref struct в локальных async функциях в C# 12+).
3. CancellationToken и отмена операций
Создание:
var cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct1, ct2);
Использование:
public async Task<string> FetchAsync(CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
using var client = new HttpClient();
return await client.GetStringAsync("https://...", ct).ConfigureAwait(false);
}
Ручная проверка:
while (!ct.IsCancellationRequested)
{
await Task.Delay(100, ct).ConfigureAwait(false);
ct.ThrowIfCancellationRequested();
}
Обработка исключения:
try
{
await operation(ct);
}
catch (OperationCanceledException ex) when (ex.CancellationToken == ct)
{
return default;
}
Важно:
- Передавать
CancellationToken явно во все асинхронные вызовы.
- Не игнорировать
ct в циклах и долгих синхронных операциях.
- Использовать
CancellationToken.Register только если нет async-альтернативы (дорого: аллокация делегата).
4. IAsyncEnumerable<T> и асинхронные потоки
Создание:
public async IAsyncEnumerable<int> GeneratePrimesAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 2; ; i++)
{
ct.ThrowIfCancellationRequested();
if (IsPrime(i))
yield return i;
await Task.Yield();
}
}
Использование:
await foreach (int prime in GeneratePrimesAsync(ct)
.ConfigureAwait(false)
.WithCancellation(ct))
{
Console.WriteLine(prime);
if (prime > 1000) break;
}
Особенности:
[EnumeratorCancellation] — внедряет CancellationToken в метод (если вызван через .WithCancellation(ct)).
ConfigureAwait(false) и WithCancellation(ct) — chainable.
yield return await — разрешено:
await foreach (var item in source)
yield return await TransformAsync(item, ct);
IAsyncEnumerator<T> реализует IAsyncDisposable → await using.
Производительность:
Channel<T> предпочтительнее, если нужна очередь producer/consumer.
AsyncEnumerable — для ленивых, pull-based потоков.
5. Продвинутые механизмы: IValueTaskSource, ManualResetValueTaskSourceCore
Когда нужно:
- Реализация
async-операций без аллокаций даже при асинхронном завершении.
- Высокопроизводительные библиотеки (сетевые протоколы, сериализаторы).
Пример:
public readonly struct MyValueTaskSource : IValueTaskSource<int>
{
private ManualResetValueTaskSourceCore<int> _core;
public ValueTask<int> GetValueAsync()
{
var vt = new ValueTask<int>(this, _core.Version);
return vt;
}
public int GetResult(short token) => _core.GetResult(token);
public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token);
public void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags)
=> _core.OnCompleted(continuation, state, token, flags);
}
ManualResetValueTaskSourceCore<T>:
- Внутренний state machine, оптимизированный для повторного использования.
- Поддерживает
SetResult, SetException, Reset.
- Не потокобезопасен — требуется внешняя синхронизация.
6. Вспомогательные типы и методы
| Тип / Метод | Назначение |
|---|
Task.CompletedTask | Завершённая Task (кэширована) — использовать вместо Task.FromResult(0) |
Task.FromResult<T>(T) | Завершённая Task<T> |
Task.FromException<T>(Exception) | Завершённая с ошибкой |
Task.FromCanceled<T>(CancellationToken) | Отменённая задача |
Task.Yield() | Возвращает управление в синхронизатор (полезно в UI для прогресса) |
Task.Delay(TimeSpan, CancellationToken) | Асинхронная задержка с отменой |
Task.WhenAll(params Task[]) | Ожидание всех, возвращает Task/Task<T[]> |
Task.WhenAny(params Task[]) | Возвращает первую завершённую |
TaskCompletionSource<T> | Вручную управляемая задача (для оборачивания событий, callback-API) |
PeriodicTimer (.NET 6+) | await timer.WaitForNextTickAsync(ct) — замена Timer + TaskCompletionSource |
Task.WaitAsync(TimeSpan, CancellationToken) (.NET 6+) | Таймаут без CancellationTokenSource |
Пример TaskCompletionSource:
var tcs = new TaskCompletionSource<bool>();
someEvent += (_, args) => tcs.SetResult(args.Success);
await tcs.Task;
7. Паттерны и анти-паттерны
✅ Рекомендованные:
- Async all the way down — не смешивать
async и .Result/.Wait().
- ConfigureAwait(false) в библиотеках — избегать захвата контекста.
- Передача
CancellationToken явно — не использовать CancellationToken.None по умолчанию в публичных API.
- Использовать
ValueTask в интерфейсах, если реализация может быть синхронной.
IAsyncDisposable + await using — для асинхронного освобождения.
❌ Анти-паттерны:
async void вне event handlers.
.Result или .Wait() в async-методах ⇒ deadlock (в presence of SynchronizationContext).
- Игнорирование
CancellationToken.
Task.Run в библиотеках без необходимости (нарушает планирование).
new Task(...).Start() — предпочтительно Task.Run или Task.Factory.StartNew с явным TaskScheduler.
Безопасное синхронное ожидание (только в крайних случаях):
var task = asyncMethod();
var result = Task.Run(() => task).GetAwaiter().GetResult();
var result = task.AsTask().GetAwaiter().GetResult();
→ Избегает deadlock, но всё равно блокирует поток.
8. Современные инструменты (.NET 6–8)
| Фича | Применение |
|---|
IProgress<T> + Progress<T> | Отчёт о прогрессе из async-методов (автоматически маршрутизирует в UI-поток) |
Channel<T> | Producer/consumer с поддержкой async (Channel.CreateBounded, CreateUnbounded) |
AsyncLock (из Nito.AsyncEx) | Асинхронная блокировка (вместо lock) |
SemaphoreSlim.WaitAsync | Асинхронное ограничение параллелизма |
Parallel.ForEachAsync (.NET 6) | Асинхронный параллелизм с контролем степени параллелизма |
Channel<T> пример:
var channel = Channel.CreateBounded<int>(10);
_ = Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync(ct))
Process(item);
});
await channel.Writer.WriteAsync(42, ct);
📚 Метапрограммирование
1. Атрибуты: синтаксис и применение
Базовый синтаксис:
[<целевой_объект>:] <ИмяАтрибута>(<позиционные_параметры>, <именованные> = <значение>)
Целевые объекты (target):
| Цель | Применяется к |
|---|
assembly | using-директиве (в начале файла) — атрибут сборки |
module | тоже — атрибут модуля (редко) |
field | полю (например, в автосвойстве) |
event | событию |
method | методу, конструктору, оператору, финализатору |
param | параметру метода |
property | свойству |
return | возвращаемому значению |
typevar | параметру типа (where T : [NotNull] object) — в обобщениях |
Пример:
[assembly: AssemblyCopyright("© 2025")]
public string Name
{
[return: NotNull]
get => _name ??= "";
[param: AllowNull]
set => _name = value;
}
2. Встроенные атрибуты .NET и C#
2.1. Управление видимостью и устареванием
| Атрибут | Параметры | Эффект |
|---|
[Obsolete] | message: string, error: bool = false | Предупреждение/ошибка компиляции при использовании |
[EditorBrowsable(EditorBrowsableState.Never)] | — | Скрывает из IntelliSense (не влияет на компиляцию) |
[Browsable(false)] | — | Для дизайнеров (WinForms/WPF) |
2.2. Условная компиляция
| Атрибут | Параметры | Эффект |
|---|
[Conditional("DEBUG")] | conditionName: string | Метод и его вызовы удаляются, если символ не определён (#define DEBUG) |
[Conditional("TRACE")] | — | То же для System.Diagnostics.Trace |
Важно: метод должен возвращать void и не иметь out-параметров (но допускает ref/in).
2.3. P/Invoke и нативный код
| Атрибут | Параметры | Эффект |
|---|
[DllImport("kernel32")] | EntryPoint, CharSet, CallingConvention, SetLastError | Импорт нативной функции |
[MarshalAs(UnmanagedType.LPStr)] | — | Указывает маршаллинг для параметра/поля/возврата |
[StructLayout(LayoutKind.Sequential, Pack = 1)] | — | Контроль макета памяти структуры |
[SuppressGCTransition] (.NET 5+) | — | Отключает переход GC-модуля для static extern методов (повышает производительность) |
2.4. Оптимизации и JIT
| Атрибут | Параметры | Эффект |
|---|
[MethodImpl(MethodImplOptions.AggressiveInlining)] | — | Подсказка JIT встраивать метод |
[MethodImpl(MethodImplOptions.NoInlining)] | — | Запрет встраивания |
[MethodImpl(MethodImplOptions.AggressiveOptimization)] | — | Применять агрессивные оптимизации даже в debug |
[ThreadStatic] | — | static поле — по одному экземпляру на поток |
[ContextStatic] | — | Одна копия на ExecutionContext (устарел, AsyncLocal<T> предпочтительнее) |
[AsyncMethodBuilder(typeof(MyBuilder))] | — | Указывает кастомный билдер для async-методов |
2.5. Nullable Reference Types (NRT) — аннотации
| Атрибут | Применяется к | Эффект |
|---|
[NotNull] | параметру, полю, свойству | После вызова значение точно не null |
[MaybeNull] | возвращаемому значению | Может вернуть null, даже если тип не помечен как T? |
[AllowNull] | параметру | Разрешает передавать null, даже если тип T (не T?) |
[DisallowNull] | параметру | Запрещает null, даже если тип T? |
[NotNullWhen(true)] | параметру out/ref | Если метод вернул true, то параметр не null |
[MaybeNullWhen(false)] | параметру out/ref | Если метод вернул false, параметр может быть null |
[DoesNotReturn] | методу | Метод никогда не возвращается (например, throw new ...) |
[DoesNotReturnIf(true)] | параметру bool | Если аргумент true, метод не возвращается |
[MemberNotNull("Field1", "Property2")] | методу | После вызова указанные члены точно не null |
[MemberNotNullWhen(true, "Field")] | методу | Если вернул true, член не null |
Пример:
[return: MaybeNull]
public T GetOrDefault<T>(string key) { ... }
public bool TryGetValue<T>(string key, [MaybeNullWhen(false)] out T value)
{
value = default;
return found;
}
2.6. Записи и обязательные члены (C# 11+)
| Атрибут | Эффект |
|---|
[SetsRequiredMembers] | Разрешает вызывать конструктор, не инициализируя required-члены — они будут установлены вызывающим кодом |
[RequiredMember] | Генерируется компилятором на типе с required-членами — не используется вручную |
Пример:
public record Person(string Name, int Age);
[SetsRequiredMembers]
public static Person CreateAdmin() => new() { Name = "Admin", Age = 99 };
2.7. Интерполированные строки (C# 10–12)
| Атрибут | Применение | Эффект |
|---|
[InterpolatedStringHandler] | ref struct | Позволяет реализовать кастомную обработку интерполированных строк без аллокаций |
[InterpolatedStringHandlerArgument("sb")] | Параметру обработчика | Передаёт дополнительные аргументы (например, StringBuilder) |
Пример:
[InterpolatedStringHandler]
public ref struct LogHandler
{
public LogHandler(int literalLength, int formattedCount, StringBuilder sb) { ... }
public void AppendLiteral(string s) => sb.Append(s);
public void AppendFormatted<T>(T value) => sb.Append(value);
}
public static void Log([InterpolatedStringHandlerArgument("")] ref LogHandler handler);
2.8. Инициализация и анализ
| Атрибут | Эффект |
|---|
[ModuleInitializer] | Статический метод без параметров — вызывается при загрузке модуля (до любого другого кода) |
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(T))] | Указывает linker'у, что тип T используется динамически — сохранить заданные члены |
[UnscopedRef] (C# 12) | Разрешает возврат ref из метода, помеченного scoped ref — отменяет ограничение |
2.9. Compile-time проверки (.NET 7–8)
| Атрибут | Применение | Эффект |
|---|
[StringSyntax(StringSyntaxAttribute.Uri)] | параметру string | Подсказывает анализаторам/IDE, что строка должна быть URI (IntelliSense, проверка в source generators) |
[StringSyntax(StringSyntaxAttribute.Json)] | — | Аналогично для JSON |
[StringSyntax(StringSyntaxAttribute.Regex)] | — | Проверка корректности регулярки на этапе компиляции |
[Experimental("MY001")] | типу/члену | Помечает как экспериментальный — требует #pragma warning disable EXXX для использования |
[ConstantExpected] (.NET 8) | параметру | Подсказка, что ожидается compile-time константа (анализатор может проверить) |
Доступные StringSyntax:
Uri, Regex, Json, DateTimeFormat, TimeFormat, NumericFormat, CompositeFormat, MailAddress
3. Пользовательские атрибуты
Создание:
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false,
Inherited = true)]
public sealed class MyAttribute : Attribute
{
public string Name { get; }
public int Priority { get; set; } = 0;
public MyAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
Правила:
- Должен наследоваться от
System.Attribute.
- Имя заканчивается на
Attribute (но при применении суффикс можно опустить: [My]).
- Позиционные параметры — только через конструктор.
- Именованные — через публичные свойства/поля.
- Параметры конструктора и свойств должны быть compile-time константами:
bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort, enum, Type, object, массивы этих типов.
Чтение в runtime (рефлексия):
var attr = typeof(MyClass).GetCustomAttribute<MyAttribute>();
if (attr?.Name == "Test") { ... }
Чтение в source generator (compile-time):
var attr = context.SemanticModel.GetDeclaredSymbol(typeSyntax)!.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.Name == "MyAttribute");
var name = attr?.ConstructorArguments[0].Value as string;
4. Рефлексия: основные API и производительность
Получение типов:
| Метод | Примечание |
|---|
typeof(T) | Compile-time, без аллокаций |
obj.GetType() | Runtime, возвращает RuntimeType |
Type.GetType("Namespace.Type, Assembly") | По строке (медленно, избегать в hot-path) |
Создание экземпляров:
| Метод | Производительность | Примечание |
|---|
new T() | наилучшая | Только для where T : new() |
Activator.CreateInstance<T>() | ~10× медленнее new | Использует кэшированный Func<T> в .NET Core+ |
Activator.CreateInstance(type) | ~100× медленнее | Возвращает object, упаковка для value types |
System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(type) | Очень быстро | Не вызывает конструктор — только для сериализаторов |
Вызов методов:
| Метод | Производительность | Примечание |
|---|
| Прямой вызов | 1× | — |
MethodInfo.Invoke | ~1000× медленнее | Аллокации, проверки безопасности |
Delegate.CreateDelegate + вызов | ~10× медленнее | Кэширование делегата критично |
Expression.Lambda(...).Compile() | ~5× медленнее | Создаёт IL-код; CompileToMethod — для статики |
Кэширование делегатов (рекомендуется):
private static readonly Func<MyClass, int> _getter =
(Func<MyClass, int>)Delegate.CreateDelegate(
typeof(Func<MyClass, int>),
typeof(MyClass).GetMethod("GetValue")!);
Альтернативы (без рефлексии):
interface + generic-ограничения.
source generators — генерация partial методов.
System.Text.Json.Serialization.Metadata — метаданные для сериализации без рефлексии.
5. Source Generators (C# 9+)
Архитектура:
[Generator]
public partial class MyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0,
transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Collect();
context.RegisterSourceOutput(provider, (spc, classes) =>
{
foreach (var c in classes)
spc.AddSource($"{c.Identifier}.g.cs", $$"""
partial class {{c.Identifier}}
{
public void GeneratedMethod() => Console.WriteLine("Hello");
}
""");
});
}
}
Incremental mode (предпочтительно):
SyntaxValueProvider.CreateSyntaxProvider — фильтрация и преобразование AST.
CompilationProvider — доступ к символам (SemanticModel).
Combine, Select, Collect, Where — композиция pipeline.
- Автоматический кэш промежуточных результатов.
Триггеры генерации:
- Изменение синтаксического дерева.
- Изменение семантики (референсы, using'и).
- Изменение дополнительных файлов (
AdditionalText).
Генерация:
- Только
partial типы и методы.
- Имена файлов:
<имя>.<хэш>.g.cs — не должны конфликтовать.
- Диагностика:
context.ReportDiagnostic(Diagnostic.Create(...)).
Пример: генерация Deconstruct для класса с [GenerateDeconstruct]:
[ModuleInitializer]
internal static void Init()
{
}
6. Директивы препроцессора
| Директива | Применение |
|---|
#define SYMBOL | Определить символ (только в начале файла) |
#undef SYMBOL | Отменить символ |
#if SYMBOL / #elif / #else / #endif | Условная компиляция |
#warning "text" | Предупреждение компиляции |
#error "text" | Ошибка компиляции |
#line 200 / #line default / #line hidden | Управление отладочной информацией (для генераторов) |
#nullable enable / disable / restore | Управление NRT на уровне файла/блока |
Стандартные символы:
DEBUG, TRACE — определяются в проекте.
NET6_0, NET7_0, NET8_0 — целевая платформа (автоматически).
WINDOWS, LINUX, BROWSER — ОС/среда (.NET 5+).
7. Caller-атрибуты (compile-time подстановка)
| Атрибут | Подставляет |
|---|
[CallerFilePath] | Путь к файлу вызова (string) |
[CallerLineNumber] | Номер строки (int) |
[CallerMemberName] | Имя метода/свойства (string) |
Пример:
public void Log(string message,
[CallerMemberName] string member = "",
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0)
{
Console.WriteLine($"{file}({line}): {member} → {message}");
}
→ Вызов Log("test") → "Program.cs(42): Main → test".